package crmdna.member;

import static crmdna.common.AssertUtils.ensure;
import static crmdna.common.AssertUtils.ensureEqual;
import static crmdna.common.AssertUtils.ensureNotNull;
import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.gson.Gson;

import crmdna.client.Client;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.DateUtils;
import crmdna.common.Utils;
import crmdna.common.contact.ContactProp;
import crmdna.group.Group;
import crmdna.program.Program;
import crmdna.program.ProgramProp;
import crmdna.programtype.ProgramType;

public class MemberSaver {
	private String client;

	private List<MemberEntity> entities = new ArrayList<>();

	private List<ContactProp> contacts = new ArrayList<>();
	private List<List<UnverifiedProgramProp>> unverifiedPrograms = new ArrayList<>();

	private Set<Long> programIdsToAdd = new HashSet<>();
	private Set<Long> programIdsToDelete = new HashSet<>();

	private Set<Long> groupIdsToAdd = new HashSet<>();
	private Set<Long> groupIdsToDelete = new HashSet<>();

	private MemberSaver() {
	}

	public static MemberSaver memberSaver(String client, List<MemberEntity> entities) {
		Client.ensureValid(client);
		ensureNotNull(entities, "entities cannot be null");

		MemberSaver saver = new MemberSaver();
		saver.client = client;
		saver.entities = entities;

		return saver;
	}	

	public static MemberSaver get(String client, MemberEntity entity) {
		List<MemberEntity> entities = new ArrayList<>(1);
		entities.add(entity);

		ensureEqual(1, entities.size());

		return memberSaver(client, entities);
	}

	public MemberSaver addProgramToAll(long programId) {
		
		Program.safeGet(client, programId);
		programIdsToAdd.add(programId);
		programIdsToDelete.remove(programId);

		return this;
	}

	public MemberSaver deleteProgramFromAll(long programId) {
		programIdsToAdd.remove(programId);
		programIdsToDelete.add(programId);

		return this;
	}

	public MemberSaver addGroupToAll(long groupId) {
		Group.safeGet(client, groupId);
		groupIdsToAdd.add(groupId);
		groupIdsToDelete.remove(groupId);

		return this;
	}

	public MemberSaver deleteGroupToAll(long groupId) {
		groupIdsToAdd.remove(groupId);
		groupIdsToDelete.add(groupId);

		return this;
	}

	public MemberSaver setContactsSameSizeList(List<ContactProp> contacts) {

		ensureNotNull(contacts, "contacts cannot be null");
		ensureEqual(entities.size(), contacts.size(), "No of contacts ["
				+ contacts.size() + "] does not match no of entities ["
				+ entities.size() + "]");

		// ensure contact is not null
		for (int i = 0; i < contacts.size(); i++) {
			ContactProp contact = contacts.get(i);
			ensureNotNull(contact, "contact(" + i + ") is null");
		}

		this.contacts = contacts;

		return this;
	}

	public MemberSaver setUnverifiedProgramsSameSizeList(
			List<List<UnverifiedProgramProp>> list) {

		ensureNotNull(list, "unverified program list cannot be null");
		ensureEqual(entities.size(), list.size(),
				"No of unverified programs [" + list.size()
						+ "] does not match no of entities [" + entities.size()
						+ "]");

		for (List<UnverifiedProgramProp> innerList : list) {
			if (innerList == null)
				continue;

			for (UnverifiedProgramProp prop : innerList) {
				if (prop == null)
					continue;

				DateUtils.ensureFormatYYYYMMDD(prop.dateYYYYMM);
			}
		}

		this.unverifiedPrograms = list;

		return this;
	}

	public MemberSaver populateDependantsAndSave() {

		if (!contacts.isEmpty()) {
			ensureEqual(entities.size(), contacts.size());
		}

		Set<Integer> changedIndexes = new HashSet<>();
		for (int i = 0; i < entities.size(); i++) {
			MemberEntity entity = entities.get(i);

			if (entity == null)
				continue;

			boolean change = entity.programIds.addAll(programIdsToAdd);
			if (change)
				changedIndexes.add(i);

			change = entity.programIds.removeAll(programIdsToDelete);
			if (change)
				changedIndexes.add(i);

			change = entity.groupIds.addAll(groupIdsToAdd);
			if (change)
				changedIndexes.add(i);

			change = entity.groupIds.removeAll(groupIdsToDelete);
			if (change)
				changedIndexes.add(i);

			if (entity.groupIds.size() == 0) {
				throw new APIException()
						.status(Status.ERROR_RESOURCE_INCORRECT)
						.message(
								"Member ["
										+ entity.getId()
										+ "] has only one group which cannot be deleted");
			}

			if (!contacts.isEmpty()) {
				ContactProp contact = contacts.get(i);
				ensureNotNull(contact, "contact(" + i + ") is null");

				change = Member.populateContactDetails(entity, contact);

				if (change)
					changedIndexes.add(i);
			}

			if (!unverifiedPrograms.isEmpty()) {
				List<UnverifiedProgramProp> list = unverifiedPrograms.get(i);
				if (list != null) {
					Collections.sort(list);
					entity.listUnverfiedProgramPropJson = new Gson()
							.toJson(list);
					
					changedIndexes.add(i);
				}
			}
		}

		ensure(changedIndexes.size() <= entities.size());

		List<MemberEntity> changedEntities = new ArrayList<>();
		for (int i : changedIndexes) {
			MemberEntity entity = entities.get(i);
			changedEntities.add(entity);
		}

		populateDependantFields(client, changedEntities);

		ofy(client).save().entities(changedEntities);
		return this;
	}
	
	private void populateDependantFields(String client,
			List<MemberEntity> memberEntities) {

		ensureNotNull(memberEntities, "memberEntities cannot be null");

		for (MemberEntity memberEntity : memberEntities) {
			ensureNotNull(memberEntity,
					"individual memberEntity cannot be null");
			populateDependantFieldsWithoutDBQuery(memberEntity);
		}

		// fields that involve database queries
		List<Long> programIds = new ArrayList<>();
		for (MemberEntity memberEntity : memberEntities) {
			programIds.addAll(memberEntity.programIds);
		}

		Map<Long, ProgramProp> programProps = Program.get(client, programIds);

		for (MemberEntity memberEntity : memberEntities) {
			memberEntity.programTypeIds.clear();
			memberEntity.practiceIds.clear();

			for (long programId : memberEntity.programIds) {
				if (programProps.containsKey(programId)) {
					ProgramProp programProp = programProps.get(programId);

					memberEntity.programTypeIds
							.add(programProp.programTypeProp.programTypeId);
				}
			}

			List<UnverifiedProgramProp> uvprops = UnverifiedProgram
					.getUnverifiedProgramPropsFromJson(memberEntity.listUnverfiedProgramPropJson);

			if (uvprops != null) {
				for (UnverifiedProgramProp unverifiedProgramProp : uvprops) {
					memberEntity.programTypeIds
							.add(unverifiedProgramProp.programTypeId);
				}
			}

			memberEntity.practiceIds = ProgramType.getPracticeIds(client,
					memberEntity.programTypeIds);
		}
	}

	private void populateDependantFieldsWithoutDBQuery(MemberEntity memberEntity) {
		// nameFirstChar
		if (memberEntity.firstName != null) {
			memberEntity.name = memberEntity.firstName.toLowerCase();
		} else if (memberEntity.email != null)
			memberEntity.name = memberEntity.email.toLowerCase();
		else if (memberEntity.mobilePhone != null)
			memberEntity.name = memberEntity.mobilePhone.toLowerCase();
		else if (memberEntity.homePhone != null)
			memberEntity.name = memberEntity.homePhone.toLowerCase();
		else if (memberEntity.officePhone != null)
			memberEntity.name = memberEntity.officePhone.toLowerCase();
		else {
			// should never come here as either first name, email or a valid
			// phone number
			// should be specified
			throw new APIException(
					Status.ERROR_RESOURCE_INCORRECT,
					"Either first name or email or a valid phone number should be specified when adding or updating member");
		}
		memberEntity.nameFirstChar = memberEntity.name.substring(0, 1);

		if ((memberEntity.firstName != null)
				&& (memberEntity.firstName.length() > 2))
			memberEntity.firstName3Char = memberEntity.firstName
					.substring(0, 3).toLowerCase();

		memberEntity.qsTags = Utils.getQSTags(memberEntity.email,
				memberEntity.firstName, memberEntity.lastName,
				memberEntity.homePhone, memberEntity.officePhone,
				memberEntity.mobilePhone);

		// phone numbers
		HashSet<String> phoneNumbers = new HashSet<>();
		if (memberEntity.mobilePhone != null)
			phoneNumbers.add(memberEntity.mobilePhone);
		if (memberEntity.officePhone != null)
			phoneNumbers.add(memberEntity.officePhone);
		if (memberEntity.homePhone != null)
			phoneNumbers.add(memberEntity.homePhone);

		memberEntity.phoneNumbers = phoneNumbers;
	}
}
